package com.sig.util;

import io.protostuff.*;
import io.protostuff.runtime.DefaultIdStrategy;
import io.protostuff.runtime.Delegate;
import io.protostuff.runtime.RuntimeEnv;
import io.protostuff.runtime.RuntimeSchema;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

/**
 * 序列化和反序列工具
 *
 * @author sig
 * @since 2019/1/26
 */
public final class ProtoStuffUtil {
    private static final ThreadLocal<Class<?>> CURRENT_LOCAL_REQUEST = new ThreadLocal<>();

    static {
        final DefaultIdStrategy strategy = (DefaultIdStrategy) RuntimeEnv.ID_STRATEGY;
        final TimestampDelegate delegate = new TimestampDelegate();
        strategy.registerDelegate(delegate);
    }

    private ProtoStuffUtil() {
    }

    public static Class<?> get() {
        return CURRENT_LOCAL_REQUEST.get();
    }

    public static void set(final Class<?> clazz) {
        CURRENT_LOCAL_REQUEST.set(clazz);
    }

    public static void clear() {
        CURRENT_LOCAL_REQUEST.set(null);
    }

    public static <T> T deserialize(final Class<T> clazz, final byte[] msgData) {
        final Schema<T> schema = RuntimeSchema.getSchema(clazz);
        final T instance;
        try {
            instance = clazz.getDeclaredConstructor().newInstance();
            ProtobufIOUtil.mergeFrom(msgData, instance, schema);
            return instance;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException("deserialize error!", e);
        }
    }

    public static <T> byte[] serialize(final Class<T> clazz, final T object) {
        if (object == null)
            throw new NullPointerException("serialize object can't be null!");
        final Schema<T> schema = RuntimeSchema.getSchema(clazz);
        final LinkedBuffer buffer = LinkedBuffer.allocate(256);
        return ProtobufIOUtil.toByteArray(object, schema, buffer);
    }

    public static <K, V> byte[] serializeMap(final Map<K, V> map) {
        final ProtoStuffMap<K, V> protoStuffMap = new ProtoStuffMap<>(map);
        return serialize(ProtoStuffMap.class, protoStuffMap);
    }

    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> deserializeMap(final byte[] msgData) {
        final ProtoStuffMap<K, V> protoStuffMap = (ProtoStuffMap<K, V>) deserialize(ProtoStuffMap.class, msgData);
        final Map<K, V> map = protoStuffMap.getMap();
        return map == null ? Collections.emptyMap() : map;
    }

    public static byte[] serializeArray(final Object[] array) {
        final ProtoStuffArray<Object> protoStuffArray = new ProtoStuffArray<>(array);
        return serialize(ProtoStuffArray.class, protoStuffArray);
    }

    @SuppressWarnings("unchecked")
    public static <T> T[] deserializeArray(final byte[] msgData) {
        final ProtoStuffArray<T> protoStuffArray = (ProtoStuffArray<T>) deserialize(ProtoStuffArray.class, msgData);
        return protoStuffArray.getArray();
    }

    public static <T> byte[] serializeCollection(final Collection<T> collection) {
        final ProtoStuffCollection<T> protoStuffCollection = new ProtoStuffCollection<>(collection);
        return serialize(ProtoStuffCollection.class, protoStuffCollection);
    }

    @SuppressWarnings("unchecked")
    public static <T> Collection<T> deserializeCollection(final byte[] msgData) {
        final ProtoStuffCollection<T> protoStuffCollection = (ProtoStuffCollection<T>) deserialize(ProtoStuffCollection.class, msgData);
        final Collection<T> collection = protoStuffCollection.getCollection();
        return collection == null ? Collections.emptyList() : collection;
    }

    public static <T> T deepCopy(final Class<T> clazz, final T object) {
        final byte[] msgData = serialize(clazz, object);
        return deserialize(clazz, msgData);
    }

    static class TimestampDelegate implements Delegate<Timestamp> {

        @Override
        public WireFormat.FieldType getFieldType() {
            return WireFormat.FieldType.FIXED64;
        }

        @Override
        public Timestamp readFrom(Input input) throws IOException {
            return new Timestamp(input.readFixed64());
        }

        @Override
        public void writeTo(Output output, int number, Timestamp timestamp, boolean repeated) throws IOException {
            output.writeFixed64(number, timestamp.getTime(), repeated);
        }

        @Override
        public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated) throws IOException {
            output.writeFixed64(number, input.readFixed64(), repeated);
        }

        @Override
        public Class<?> typeClass() {
            return Timestamp.class;
        }
    }

    static class ProtoStuffMap<K, V> {
        private Map<K, V> map;

        public ProtoStuffMap() {
        }

        public ProtoStuffMap(Map<K, V> map) {
            this.map = map;
        }

        public Map<K, V> getMap() {
            return map;
        }

        public void setMap(Map<K, V> map) {
            this.map = map;
        }

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
        }
    }

    static class ProtoStuffArray<T> {
        private T[] array;

        public ProtoStuffArray() {
        }

        public ProtoStuffArray(T[] array) {
            this.array = array;
        }

        public T[] getArray() {
            return array;
        }

        public void setArray(T[] array) {
            this.array = array;
        }

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
        }
    }

    static class ProtoStuffCollection<T> {
        private Collection<T> collection;

        public ProtoStuffCollection() {
        }

        public ProtoStuffCollection(Collection<T> collection) {
            this.collection = collection;
        }

        public Collection<T> getCollection() {
            return collection;
        }

        public void setCollection(Collection<T> collection) {
            this.collection = collection;
        }

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
        }
    }
}
